//Include library header
#include <fstream>

#include "texture.h"
#include "base/logging.h"
#include "stb/include/stb_image.h"

std::ostream &operator<<(std::ostream& out, const tri::TImageData& m) {
    out<<"<ImageData : ";
    out<<"width = "<<m.width;
    out<<" height = "<<m.height;
    out<<" format = "<<m.format;
    out<<" bits = "<<m.bits;
    return out;
}

namespace tri {

TImageData::TImageData() :
	 width(0), height(0), bits(0), format(TRI_RGB),data(NULL) {
}

TImageData::~TImageData() {
	clear();
}

void TImageData::clear(){
	stbi_image_free(data);
	width = 0;
	height = 0;
	bits = 0;
	format = TRI_RGB;
	data = NULL;
}


TriTextureOverSizeException::TriTextureOverSizeException(const char* msg) :
	message(msg) {
}

const char * TriTextureOverSizeException::what() {
	return message;
}

void writeImageDataFile(std::string file,std::vector<std::string>& images){	
	std::fstream binary_file(file.c_str(),std::ios::out|std::ios::binary);
	
	int HEADER = 1200;
	binary_file.write(reinterpret_cast<char *>(&HEADER),sizeof(int));
	
	int IMAGE_COUNT = images.size();
	binary_file.write(reinterpret_cast<char *>(&IMAGE_COUNT),sizeof(int));
	tri::TImageData img;
	for(int i = 0;i<IMAGE_COUNT;i++){
		int NAME_SIZE = images[i].size();
		binary_file.write(reinterpret_cast<char *>(&NAME_SIZE),sizeof(int));
		binary_file.write(images[i].c_str(),sizeof(char)*NAME_SIZE);
		
		if(tri::loadImage(&img,images[i])){
			int CHUNK_SIZE = ( sizeof(int)*4 ) + img.width*img.height*img.bits;
			
			binary_file.write(reinterpret_cast<char *>(&CHUNK_SIZE),sizeof(int));
			binary_file.write(reinterpret_cast<char *>(&img.width),sizeof(int));
			binary_file.write(reinterpret_cast<char *>(&img.height),sizeof(int));
			binary_file.write(reinterpret_cast<char *>(&img.bits),sizeof(int));
			binary_file.write(reinterpret_cast<char *>(&img.format),sizeof(int));
			binary_file.write(reinterpret_cast<char *>(img.data),img.width*img.height*img.bits);
			
			freeImage(&img);
		}else{
			int _fail = 0;
			binary_file.write(reinterpret_cast<char *>(&_fail),sizeof(int));
		}
	}
	
	binary_file.close();
	
}

bool readImageDataFile(std::string data_file,std::string image_file,tri::TImageData* img){
	if (!tri::sys::isFile(data_file)) {
		std::string msg = "File does not exists: ";
		LOG_WARNING(msg+data_file);
		return false;
	}
	
	std::fstream binary_file(data_file.c_str(),std::ios::in|std::ios::binary);
	
	int HEADER=0;
	binary_file.read(reinterpret_cast<char *>(&HEADER),sizeof(int));
	
	int IMAGE_COUNT=0;
	binary_file.read(reinterpret_cast<char *>(&IMAGE_COUNT),sizeof(int));
	
	for(int i = 0;i<IMAGE_COUNT;i++){
		
		int NAME_SIZE=0;
		binary_file.read(reinterpret_cast<char *>(&NAME_SIZE),sizeof(int));
		char namebuf[300];
		std::string name = "";
		binary_file.read(namebuf,sizeof(char)*NAME_SIZE);
		name.append(namebuf,NAME_SIZE);
		int CHUNK_SIZE = 0;
		binary_file.read(reinterpret_cast<char *>(&CHUNK_SIZE),sizeof(int));
		if (name == image_file && CHUNK_SIZE > 0){
			binary_file.read(reinterpret_cast<char *>(&img->width),sizeof(int));
			binary_file.read(reinterpret_cast<char *>(&img->height),sizeof(int));
			binary_file.read(reinterpret_cast<char *>(&img->bits),sizeof(int));
			binary_file.read(reinterpret_cast<char *>(&img->format),sizeof(int));
			int data_size = img->width*img->height*img->bits;
			img->data = new unsigned char[data_size];
			binary_file.read(reinterpret_cast<char *>(img->data),data_size);
			binary_file.close();
			return true;
		}else{
			binary_file.seekg (CHUNK_SIZE, std::ios::cur);
		}
	}
	
	binary_file.close();
	return false;
}

bool readImageDataFileFiles(std::string data_file,std::vector<std::string>& list){
	if (!tri::sys::isFile(data_file)) {
		std::string msg = "File does not exists: ";
		LOG_WARNING(msg+data_file);
		return false;
	}
	std::fstream binary_file(data_file.c_str(),std::ios::in|std::ios::binary);
	list.clear();
	int HEADER=0;
	binary_file.read(reinterpret_cast<char *>(&HEADER),sizeof(int));
	
	int IMAGE_COUNT=0;
	binary_file.read(reinterpret_cast<char *>(&IMAGE_COUNT),sizeof(int));
	
	for(int i = 0;i<IMAGE_COUNT;i++){
		
		int NAME_SIZE=0;
		binary_file.read(reinterpret_cast<char *>(&NAME_SIZE),sizeof(int));
		char namebuf[300];
		std::string name = "";
		binary_file.read(namebuf,sizeof(char)*NAME_SIZE);
		name.append(namebuf,NAME_SIZE);
		list.push_back(name);
		int CHUNK_SIZE = 0;
		binary_file.read(reinterpret_cast<char *>(&CHUNK_SIZE),sizeof(int));
		binary_file.seekg (CHUNK_SIZE, std::ios::cur);
	}
	
	binary_file.close();
	return false;
}

bool loadImage(TImageData *im, std::string file) {
	if (!tri::sys::isFile(file)) {
		std::string data_file = TTexture::getImageFileFromImage(file);
		LOG_INFO("Checking data " + data_file + " for image ..."+file);
		if (data_file != ""){
			if(readImageDataFile(data_file,file,im)){
				return true;
			}
		}
		std::string msg = "Image Does not exists: ";
		LOG_WARNING(msg+file);
		//exit(1);
		return false;
	}
	im->data = stbi_load(file.c_str(), &im->width, &im->height, &im->bits, 0);
	im->format = TRI_RGBA;
	if (im->bits == 3) {
		im->format = TRI_RGB;
	}
	return im->data!=NULL;
}

void freeImage(TImageData *im) {
	if (im->data!=NULL) {
		delete [] im->data;
		im->data = NULL;
	}
}

//=================================================================================

std::string TTexture::mImageDataFile = "";
std::map< std::string,std::vector<std::string> > TTexture::mImageDataIndex;

TTexture::TTexture() :
	id(0), mipmap(false) {
}
TTexture::~TTexture() {
	if (this->id > 0) {
		glDeleteTextures(1, &this->id);
		LOG_INFO("Destoying Texture.. "+mFile);
	}
}


unsigned int TTexture::getTexID(){
	return this->id;
}

void TTexture::load(std::string file, bool mipmap,bool filter ) {
	TImageData img;
	LOG_INFO("Loading Texture.. "+file);
	if (loadImage(&img, file)) {
		mFile = file;
		this->load(&img, mipmap,filter);
	}else{
		LOG_WARNING("Unable to load.. "+file);
	}
	freeImage(&img);

}

void TTexture::load(TImageData *im, bool _mipmap,bool filter ) {
	this->del();
	_gen_id();
	if (filter){
	//apply filters
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	}else{
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	}
	//copy demensions
	width = im->width;
	height = im->height;
	raw_width = im->width;
	raw_height = im->height;

	format = im->format;
	bits = im->bits;
	mipmap = _mipmap;
	if (mipmap && filter) {
		glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
                 GL_LINEAR_MIPMAP_LINEAR );
		// when texture area is large, bilinear filter the original
		glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
	}

	if (!tri::utils::isPow2(width)||!tri::utils::isPow2(height)) {
		raw_width = tri::utils::nextPow2(width);
		raw_height = tri::utils::nextPow2(height);
		unsigned char* buf = new unsigned char[raw_width*raw_height*im->bits];

		if(mipmap){
			gluBuild2DMipmaps( GL_TEXTURE_2D, 
				im->bits, 
				raw_width,
				raw_height,
				im->format, 
				GL_UNSIGNED_BYTE,
				buf );
		}else{
			glTexImage2D(GL_TEXTURE_2D,
				0,
				im->bits,
				raw_width,
				raw_height,
				0,
				im->format,
				GL_UNSIGNED_BYTE,
				buf);
		}
		
		
		delete []buf;
		update(im);

	} else {
		if(mipmap){
			gluBuild2DMipmaps( GL_TEXTURE_2D, 
					im->bits, 
					raw_width,
					raw_height,
					im->format, 
					GL_UNSIGNED_BYTE,
					im->data );
		}else{
			glTexImage2D(GL_TEXTURE_2D,
					0,
					im->bits,
					im->width,
					im->height,
					0,
					im->format,
					GL_UNSIGNED_BYTE,
					im->data);
		}
		tc = TRectangle(0, 0, (float)width/raw_width, (float)height/raw_height
				);
	}

}

void TTexture::setImageDataFile(std::string file){
	mImageDataFile = file;
}

std::string TTexture::getImageDataFile(){
	return mImageDataFile;
}

void TTexture::addImageDataFile(std::string file){
	bool ret = false;
	std::map<std::string, std::vector<std::string> >::iterator it;
	for ( it=TTexture::mImageDataIndex.begin() ; it != TTexture::mImageDataIndex.end(); it++ ){
		if ( (*it).first == file ){
			ret = true;
		}
	}
	
	if(!ret){
		readImageDataFileFiles(file,TTexture::mImageDataIndex[file]);
	}
}

std::string TTexture::getImageFileFromImage(std::string image){
	std::map<std::string, std::vector<std::string> >::iterator it;
	for ( it=TTexture::mImageDataIndex.begin() ; it != TTexture::mImageDataIndex.end(); it++ ){
		for (unsigned int i =0; i<(*it).second.size();i++ ){
			if((*it).second[i]==image){
				return (*it).first;
			}
		}
	}
	return "";
}

void TTexture::load(int w, int h, int mode) {
	TImageData img;
	img.width = w;
	img.height = h;
	img.data = new unsigned char[w*h*4];
	img.bits = 4;
	load(&img);
	delete []img.data;
}

void TTexture::getTextureSize(int& _width, int &_height) {
	_width = this->width;
	_height = this->height;
}

void TTexture::update(TImageData *im) {

	if (mipmap) {
		return;
	}

	if (id == 0) {
		return;
	}
	if (im->width > raw_width ||im->height > raw_height) {
		LOG_SEVERE("The image size is bigger than the current texture!!!!");
		exit(1);
	}
	if (im->format != format || im->bits != bits) {
		LOG_SEVERE("The image format of pixel bit size differs from current texture!!!!");
		exit(1);
	}
	width = im->width;
	height = im->height;
	glBindTexture(GL_TEXTURE_2D, id);
	glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, im->width, im->height,
	GL_RGBA, GL_UNSIGNED_BYTE, im->data);
	tc = TRectangle(0, 0, (float)width/raw_width, (float)height/raw_height);

}

void TTexture::update(std::string file) {
	TImageData img;
	if (loadImage(&img, file)) {
		this->update(&img);
	}
	freeImage(&img);
}

void TTexture::_gen_id() {
	if (id == 0) {
		glGenTextures(1, &id);
		glBindTexture(GL_TEXTURE_2D, id);
	}
}

bool TTexture::created() {
	return id!=0;
}

void TTexture::del() {
	if (this->id > 0) {
		glDeleteTextures(1, &this->id);
	}
}

void TTexture::bind() {
	if (this->id > 0) {
		glBindTexture(GL_TEXTURE_2D, id);
	}
}

TRectangle TTexture::getTC() {
	if (!created()) {
		return TRectangle();
	}
	return tc;
}

float TTexture::getAspectRatio() {
	return (float)getWidth()/(float)getHeight();
}

int TTexture::getWidth() {
	return (int)(tc.value[2]*raw_width)-(int)(tc.value[0]*raw_width);
}

int TTexture::getHeight() {
	return (int)(tc.value[3]*raw_height)-(int)(tc.value[1]*raw_height);
}

void TTexture::resetTC() {
	tc = TRectangle(0, 0, (float)width/raw_width, (float)height/raw_height);
}

void TTexture::addSpriteData(std::string key, TRectangle rect) {
	sprites[key] = rect;
}

void TTexture::setSprite(std::string key) {
	bool has_sprite = false;
	std::map<std::string, tri::TRectangle >::iterator it;
	for ( it=sprites.begin() ; it != sprites.end(); it++ ){
		if ((*it).first == key){
			has_sprite = true;
			break;
		}
	}
	
	if (has_sprite) {
		float r[4];
		sprites[key].get(r);
		tc = TRectangle((float)r[0]/raw_width, (float)r[1]/raw_height,
				(float)r[2]/raw_width, (float)r[3]/raw_height*0.99);
	}
}

void TTexture::setSprite(TRectangle rect) {
	float r[4];
	rect.get(r);
	tc = TRectangle((float)r[0]/raw_width, (float)r[1]/raw_height, (float)r[2]
			/raw_width, (float)r[3]/raw_height*0.99);
}



void applyTexture(std::string _image) {
	tri::TTexture* tex= NULL;
	if (_image.compare("")!=0) {
		tex = tri::getTexture(_image);

		if (tex!=NULL) {
			if (!tex->created()) {
				tex->load(_image);
			}
			tex->bind();
		} else if (tri::sys::isFile(_image)) {
			tri::createTexture(_image);
			tex = tri::getTexture(_image);
			if (tex!=NULL) {
				tex->load(_image);
				tex->bind();
			}
		} else {
			tri::unbindAllTextures();
		}

	} else {
		tri::unbindAllTextures();
	}
}

TTextureCache TTextureCache::cache = TTextureCache();

TTextureCache::TTextureCache() {
}
TTextureCache::~TTextureCache() {
}
bool TTextureCache::containes(std::string key) {
	return _map.count(key)>0;
}

void TTextureCache::add(std::string key) {
	if (!containes(key)) {
		_map[key] = TTexture();
	}
}

void TTextureCache::remove(std::string key) {
	if (containes(key)) {
		_map[key].del();
		_map.erase(key);
	}
}

TTexture* TTextureCache::get(std::string key) {
	if (containes(key)) {
		return &_map[key];
	}

	return NULL;
}

void createTexture(std::string key) {
	TTextureCache::cache.add(key);
}

void createTexture(std::string key, std::string file,bool mipmap,bool filter) {
	TTextureCache::cache.add(key);
	TTextureCache::cache.get(key)->load(file,mipmap,filter);
}

void deleteTexture(std::string key) {
	TTextureCache::cache.remove(key);
}

TTexture* getTexture(std::string key) {
	return TTextureCache::cache.get(key);
}

//tri::TTexture* getTexture(std::string _image) {
//	tri::TTexture* tex= NULL;
//	if (_image.compare("")!=0) {
//		tex = tri::_getTexture(_image);
//
//		if (tex!=NULL) {
//			if (!tex->created()) {
//				tex->load(_image);
//			}
//		} else if (tri::sys::isFile(_image)) {
//			tri::createTexture(_image);
//			tex = tri::_getTexture(_image);
//			if (tex!=NULL) {
//				tex->load(_image);
//			}
//
//		} else {
//			tri::unbindAllTextures();
//		}
//
//	} else {
//		tri::unbindAllTextures();
//	}
//
//	return tex;
//}

void unbindAllTextures() {
	glBindTexture(GL_TEXTURE_2D, 0);
}

}

